/*
 * The MIT License
 * Copyright (c) 2011 kc4.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.kc4.sso.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.kc4.sso.auth.SSOContextParameters;
import org.kc4.sso.auth.SSOProviderInitParameters;
import org.kc4.sso.auth.section.InterruptedSectionException;
import org.kc4.sso.auth.section.provider.AcceptAuthorizationTokenSection;
import org.kc4.sso.auth.section.provider.AcceptSignoffTokenSection;
import org.kc4.sso.auth.section.provider.ActiveClientOnValidSessionSection;
import org.kc4.sso.auth.section.provider.FailedSignonOnInvalidSessionSection;
import org.kc4.sso.auth.section.provider.QuittedClientOnValidSessionSection;
import org.kc4.sso.auth.section.provider.RejectUncertifiedClientSection;
import org.kc4.sso.auth.section.provider.SucceededSignonOnInvalidSessionSection;
import org.kc4.util.FilterInitParametersMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * シングルサインオンプロバイダフィルタ。<br />
 * フィルタの適用例は次のとおりです。
 * <pre>{@code
 *<filter>
 *	<display-name>SSOProviderFilter</display-name>
 *	<filter-name>SSOProviderFilter</filter-name>
 *	<filter-class>org.kc4.sso.filter .SSOProviderFilter</filter-class>
 *	<init-param>
 *		<description>
 *			フィルタから除外するパターンを設定します。
 *			ワイルドカードは先頭と末尾のみに使用することができます。
 *		</description>
 *		<param-name>IgnoreURI</param-name>
 *		<param-value>/css/*,/js/*</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			サインオンフォームのページを設定します。
 *			必須設定項目です。
 *		</description>
 *		<param-name>SignonPage</param-name>
 *		<param-value>/signon.html</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			ユーザーのサインオン認証を行うクラスを設定します。
 *			指定するクラスにはアクセス可能な引数なしのコンストラクタが
 *			定義されている必要があります。
 *			必須設定項目です。
 *		</description>
 *		<param-name>Authenticator</param-name>
 *		<param-value>org.kc4.sso.auth.Authenticator</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			SSOセッションストアクラスを設定します。
 *			指定するクラスにはアクセス可能な引数なしのコンストラクタが
 *			定義されている必要があります。
 *			必須設定項目です。
 *		</description>
 *		<param-name>SSOSessionStore</param-name>
 *		<param-value></param-value>
 *	</init-param>
 *	<!--
 *	<init-param>
 *		<description>
 *			SSOクライアントとして認証利用を許可するホワイトリストを設定します。
 *		</description>
 *		<param-name>WhiteClients</param-name>
 *		<param-value>http://hostXXX/appname/,http://hostYYY/appname/</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			コールバックのリクエストURLパラメーター名を設定します。
 *			初期値は "_callback" です。
 *		</description>
 *		<param-name>UrlRequestNameOfCallback</param-name>
 *		<param-value>_callback</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			公開鍵のリクエストURLパラメーター名を設定します。
 *			初期値は "_pubkey" です。
 *		</description>
 *		<param-name>UrlRequestNameOfPublickey</param-name>
 *		<param-value>_pubkey</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			コンテキストURLのリクエストURLパラメーター名を設定します。
 *			初期値は "_ctxurl" です。
 *		</description>
 *		<param-name>UrlRequestNameOfContextUrl</param-name>
 *		<param-value>_ctxurl</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			サインオントークンのレスポンスURLパラメーター名を設定します。
 *			初期値は "_token" です。
 *		</description>
 *		<param-name>UrlResponseNameOfSignonToken</param-name>
 *		<param-value>_token</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			レルムのリクエストフォームパラメーター名を設定します。
 *			初期値は "_realm" です。
 *		</description>
 *		<param-name>FormRequestNameOfRealm</param-name>
 *		<param-value>_realm</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			ユーザー名のリクエストフォームパラメーター名を設定します。
 *			初期値は "_username" です。
 *		</description>
 *		<param-name>FormRequestNameOfUsername</param-name>
 *		<param-value>_username</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			パスワードのリクエストフォームパラメーター名を設定します。
 *			初期値は "_password" です。
 *		</description>
 *		<param-name>FormRequestNameOfPassword</param-name>
 *		<param-value>_password</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			認証トークンのレスポンスURLパラメーター名を設定します。
 *			初期値は "_auth" です。
 *		</description>
 *		<param-name>UrlResponseNameOfAuthorizationToken</param-name>
 *		<param-value>_auth</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			暗号化された共通鍵のレスポンスURLパラメーター名を設定します。
 *			初期値は "_comkey" です。
 *		</description>
 *		<param-name>UrlResponseNameOfEncryptedCommonkey</param-name>
 *		<param-value>_comkey</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			認証トークンのリクエストフォームパラメーター名を設定します。
 *			初期値は "_auth" です。
 *		</description>
 *		<param-name>FormRequestNameOfAuthorizationToken</param-name>
 *		<param-value>_auth</param-value>
 *	</init-param>
 *	<init-param>
 *		<description>
 *			認証トークンのセッション属性名を設定します。
 *			初期値は "_auth" です。
 *		</description>
 *		<param-name>SessionNameOfAuthorizationToken</param-name>
 *		<param-value>_auth</param-value>
 *	</init-param>
 *	-->
 *</filter>
 *<filter-mapping>
 *	<filter-name>SSOProviderFilter</filter-name>
 *	<url-pattern>/*</url-pattern>
 *</filter-mapping>
 * }</pre>
 *
 * @author yoichi kikuchi
 * @since 1.0.0
 */
public class SSOProviderFilter extends IgnorableFilter {

	/**
	 * エンドユーザーのサインオンシーケンス中にセッション保持されるオブジェクト。
	 *
	 * @author yoichi kikuchi
	 */
	public static class EnduserSignonSequence {

		private String publickey;
		private String callback;
		private String contextUrl;
		/**
		 * publickey を取得します。
		 *
		 * @return publickey オブジェクト。
		 */
		public String getPublickey() {
			return this.publickey;
		}
		/**
		 * publickey を設定します。
		 *
		 * @param publickey publickey オブジェクト。
		 */
		public void setPublickey(String publickey) {
			this.publickey = publickey;
		}
		/**
		 * callback を取得します。
		 *
		 * @return callback オブジェクト。
		 */
		public String getCallback() {
			return this.callback;
		}
		/**
		 * callback を設定します。
		 *
		 * @param callback callback オブジェクト。
		 */
		public void setCallback(String callback) {
			this.callback = callback;
		}
		/**
		 * contextUrl を取得します。
		 *
		 * @return contextUrl オブジェクト。
		 */
		public String getContextUrl() {
			return this.contextUrl;
		}
		/**
		 * contextUrl を設定します。
		 *
		 * @param contextUrl contextUrl オブジェクト。
		 */
		public void setContextUrl(String contextUrl) {
			this.contextUrl = contextUrl;
		}
	}

	private static final Logger LOGGER = LoggerFactory.getLogger(SSOProviderFilter.class);

	/**
	 * Default constructor.
	 */
	public SSOProviderFilter() {}

	/**
	 * @see Filter#init(FilterConfig)
	 * @see IgnorableFilter#init(FilterConfig)
	 */
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {

		super.init(filterConfig);

		// コンテキストのロード
		SSOContextParameters.getInstance(filterConfig.getServletContext());

		// フィルタ構成のロード
		SSOProviderInitParameters.getInstance(
			new FilterInitParametersMap(filterConfig));
	}

	/**
	 * @see Filter#destroy()
	 */
	@Override
	public void destroy() {}

	/**
	 * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

		if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {

			HttpServletRequest req = (HttpServletRequest) request;
			HttpServletResponse res = (HttpServletResponse) response;

			if (LOGGER.isTraceEnabled()) {

				LOGGER.trace(String.format("ignore filter uri=\"%s\"", req.getRequestURI()));
			}

			if (this.isIgnoreURI(req) || this.isSignonPage(req)) {

				chain.doFilter(request, response);
				return;
			}

			// サインオフトークン受付
			try {

				new AcceptSignoffTokenSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// 認証トークン受付
			try {

				new AcceptAuthorizationTokenSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// ホワイトリストが設定されている場合、これを参照して許可されていないクライアントを除外する
			try {

				new RejectUncertifiedClientSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// SSOセッション有効 > SSOクライアント有効
			try {

				new ActiveClientOnValidSessionSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// SSOセッション有効 > SSOクライアント無効
			try {

				new QuittedClientOnValidSessionSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// SSOセッション無効 > サインオンシーケンス有効、かつ サインオン成功
			try {

				new SucceededSignonOnInvalidSessionSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}

			// SSOセッション無効 > サインオンシーケンス無効、または サインオン失敗
			try {

				new FailedSignonOnInvalidSessionSection(req, res).execute();
			}
			catch (InterruptedSectionException e) {

				return;
			}
		}

		// リクエスト、レスポンスが HttpServletRequest、および
		// HttpServletResponse と互換性のない場合は処理しない
		// chain.doFilter(request, response);
	}

	/**
	 * サインオンページへのリクエストかどうかを取得します。
	 *
	 * @param req リクエスト。
	 * @return サインオンページへのリクエストの場合は true、それ以外の場合は false。
	 */
	protected boolean isSignonPage(HttpServletRequest req) {

		String realm = req.getParameter(SSOContextParameters.getInstance().getNamingOfRealm());
		String username = req.getParameter(SSOContextParameters.getInstance().getNamingOfUsername());
		String password = req.getParameter(SSOContextParameters.getInstance().getNamingOfPassword());

		return this.isSignonPage(req.getContextPath(), req.getRequestURI(),
			realm, username, password);
	}

	/**
	 * サインオンページへのリクエストかどうかを取得します。
	 *
	 * @param contextPath コンテキストパス。
	 * @param uri 検査する URI。
	 * @param realm 認証範囲。
	 * @param username ユーザー名。
	 * @param password パスワード。
	 * @return サインオンページへのリクエストの場合は true、それ以外の場合は false。
	 */
	protected boolean isSignonPage(String contextPath, String uri, String realm, String username, String password) {

		if (contextPath.length() > uri.length()) return false;

		String targetUri = uri.substring(contextPath.length());

		return targetUri.equals(SSOProviderInitParameters
			.getInstance().getSignonPage()) &&
			(realm == null || realm.isEmpty()) &&
			(username == null || username.isEmpty()) &&
			(password == null || password.isEmpty());
	}

//	/**
//	 * リクエストされたクライアントコンテキストURLホワイトリストに存在するかどうか検査します。
//	 *
//	 * @param contextUrl コンテキストURL。
//	 * @return ホワイトリストに存在する場合は true、それ以外の場合は false。
//	 */
//	protected boolean isArrowedClient(String contextUrl) {
//
//		// ホワイトリストに存在
//		for (String whiteClient : SSOProviderInitParameters
//			.getInstance().getWhiteClients()) {
//
//			if (whiteClient.equals(contextUrl)) {
//
//				return true;
//			}
//		}
//
//		return false;
//	}

//	/**
//	 * 認証トークンを生成します。
//	 *
//	 * @param store SSOセッションストア。
//	 * @return 認証トークン。
//	 */
//	protected String newAuthorizationToken(SSOSessionStore store) {
//
//		String token = null;
//
//		while (token == null || store.exist(token)) {
//
//			token = UUID.randomUUID().toString().replaceAll("-", EMPTY);
//		}
//
//		return token;
//	}

//	/**
//	 * サインオントークンを生成します。
//	 *
//	 * @param session セッション。
//	 * @return サインオントークン。
//	 */
//	protected String newSignonToken(HttpSession session) {
//
//		String token = null;
//
//		while (token == null || session.getAttribute(token) != null) {
//
//			token = UUID.randomUUID().toString().replaceAll("-", EMPTY);
//		}
//
//		return token;
//	}


}
